package jsr292.java.lang.invoke;

import java.lang.reflect.Array;

import jsr292.weaver.Debug;
import jsr292.weaver.opt.OptimizerContext;
import jsr292.weaver.opt.OptimizerSnippets;
import jsr292.weaver.opt.OptimizerVarAllocator;

final class MHVarargsCollector extends MethodHandle {
    private final MethodHandle target;
    private final int targetLength;
    private final Class<?> componentType;
    
    public MHVarargsCollector(MethodType type,  Class<?> componentType, MethodHandle target) {
        super(type);
        this.target = target;
        this.targetLength = target.type().parameterCount();
        this.componentType = componentType;
    }
    
    private Object[] createArray(int length) {
        return (Object[])Array.newInstance(componentType, length);
    }
    
    @Override
    MethodHandle asVarargsCollector0(Class<?> arrayType) {
        Class<?> componentType = arrayType.getComponentType();
        if (componentType == this.componentType) {
            return this;
        }
        
        int length = methodType.parameterCount();
        if (length == 0 ||
            !methodType.parameterType(length - 1).isAssignableFrom(arrayType)) {
            throw new IllegalArgumentException("the array type is not assignable to last parameter");
        }
        
        return new MHVarargsCollector(methodType, arrayType.getComponentType(), target);
    }
    
    @Override
    public MethodHandle asType(MethodType newType) {
        MethodType type = this.type();
        int parameterCount = type.parameterCount();
        
        // same number of parameters, use MethodHandle.asType()
        if (parameterCount == newType.parameterCount()) {
            return super.asType(newType);
        }
            
        int arrayLength = newType.parameterCount() - parameterCount + 1;
        
        try {
            return asCollector0(componentType, arrayLength).asType(newType);
        } catch(IllegalArgumentException e) {
            throw (WrongMethodTypeException)new WrongMethodTypeException().initCause(e);
        }
    }
    
    @Override
    public Object $invoke$() throws Throwable {
        if (targetLength == 0) {
            return target.$invoke$();
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1) throws Throwable {
        if (targetLength == 1) {
          return target.$invoke$(o1);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1, Object o2) throws Throwable {
        switch(targetLength) {
        case 1:
            Object[] array = createArray(2);
            array[0] = o1;
            array[1] = o2;
            return target.$invoke$((Object)array);
        case 2:
            return target.$invoke$(o1, o2);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1, Object o2, Object o3) throws Throwable {
        Object[] array;
        switch(targetLength) {
        case 1:
            array = createArray(3);
            array[0] = o1;
            array[1] = o2;
            array[2] = o3;
            return target.$invoke$((Object)array);
        case 2:
            array = createArray(2);
            array[0] = o2;
            array[1] = o3;
            return target.$invoke$(o1, array);
        case 3:
            return target.$invoke$(o1, o2, o3);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1, Object o2, Object o3, Object o4) throws Throwable {
        Object[] array;
        switch(targetLength) {
        case 1:
            array = createArray(4);
            array[0] = o1;
            array[1] = o2;
            array[2] = o3;
            array[3] = o4;
            return target.$invoke$((Object)array);
        case 2:
            array = createArray(3);
            array[0] = o2;
            array[1] = o3;
            array[2] = o4;
            return target.$invoke$(o1, array);
        case 3:
            array = createArray(2);
            array[0] = o3;
            array[1] = o4;
            return target.$invoke$(o1, o2, array);
        case 4:
            return target.$invoke$(o1, o2, o3, o4);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1, Object o2, Object o3, Object o4, Object o5) throws Throwable {
        Object[] array;
        switch(targetLength) {
        case 1:
            array = createArray(5);
            array[0] = o1;
            array[1] = o2;
            array[2] = o3;
            array[3] = o4;
            array[4] = o5;
            return target.$invoke$((Object)array);
        case 2:
            array = createArray(4);
            array[0] = o2;
            array[1] = o3;
            array[2] = o4;
            array[3] = o5;
            return target.$invoke$(o1, array);
        case 3:
            array = createArray(3);
            array[0] = o3;
            array[1] = o4;
            array[2] = o5;
            return target.$invoke$(o1, o2, array);
        case 4:
            array = createArray(2);
            array[0] = o4;
            array[1] = o5;
            return target.$invoke$(o1, o2, o3, array);
        case 5:
            return target.$invoke$(o1, o2, o3, o4, o5);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1, Object o2, Object o3, Object o4, Object o5, Object o6) throws Throwable {
        Object[] array;
        switch(targetLength) {
        case 1:
            array = createArray(6);
            array[0] = o1;
            array[1] = o2;
            array[2] = o3;
            array[3] = o4;
            array[4] = o5;
            array[5] = o6;
            return target.$invoke$((Object)array);
        case 2:
            array = createArray(5);
            array[0] = o2;
            array[1] = o3;
            array[2] = o4;
            array[3] = o5;
            array[4] = o6;
            return target.$invoke$(o1, array);
        case 3:
            array = createArray(4);
            array[0] = o3;
            array[1] = o4;
            array[2] = o5;
            array[3] = o6;
            return target.$invoke$(o1, o2, array);
        case 4:
            array = createArray(3);
            array[0] = o4;
            array[1] = o5;
            array[2] = o6;
            return target.$invoke$(o1, o2, o3, array);
        case 5:
            array = createArray(2);
            array[0] = o5;
            array[1] = o6;
            return target.$invoke$(o1, o2, o3, o4, array);
        case 6:
            return target.$invoke$(o1, o2, o3, o4, o5, o6);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object o1, Object o2, Object o3, Object o4, Object o5, Object o6, Object o7) throws Throwable {
        Object[] array;
        switch(targetLength) {
        case 1:
            array = createArray(7);
            array[0] = o1;
            array[1] = o2;
            array[2] = o3;
            array[3] = o4;
            array[4] = o5;
            array[5] = o6;
            array[6] = o7;
            return target.$invoke$((Object)array);
        case 2:
            array = createArray(6);
            array[0] = o2;
            array[1] = o3;
            array[2] = o4;
            array[3] = o5;
            array[4] = o6;
            array[5] = o7;
            return target.$invoke$(o1, array);
        case 3:
            array = createArray(5);
            array[0] = o3;
            array[1] = o4;
            array[2] = o5;
            array[3] = o6;
            array[4] = o7;
            return target.$invoke$(o1, o2, array);
        case 4:
            array = createArray(4);
            array[0] = o4;
            array[1] = o5;
            array[2] = o6;
            array[3] = o7;
            return target.$invoke$(o1, o2, o3, array);
        case 5:
            array = createArray(3);
            array[0] = o5;
            array[1] = o6;
            array[1] = o7;
            return target.$invoke$(o1, o2, o3, o4, array);
        case 6:
            array = createArray(2);
            array[0] = o6;
            array[1] = o7;
            return target.$invoke$(o1, o2, o3, o4, o5, array);
        case 7:
            return target.$invoke$(o1, o2, o3, o4, o5, o6, o7);
        }
        throw fail();
    }
    @Override
    public Object $invoke$(Object... rest) throws Throwable {
        int targetLength = this.targetLength;
        int restLength = rest.length;
        if (targetLength == restLength) {
            return target.$invoke$(rest);
        }
        
        int length = restLength - targetLength;
        if (length > 0) {
            Object[] array = createArray(length);
            System.arraycopy(rest, targetLength, array, 0, length);
            
            switch(targetLength) {
            case 0:
                throw fail();
            case 1:
                return target.$invoke$((Object)array);
            case 2:
                return target.$invoke$(rest[0], array);
            case 3:
                return target.$invoke$(rest[0], rest[1], array);
            case 4:
                return target.$invoke$(rest[0], rest[1], rest[2], array);
            case 5:
                return target.$invoke$(rest[0], rest[1], rest[2], rest[3], array);
            case 6:
                return target.$invoke$(rest[0], rest[1], rest[2], rest[3], rest[4], array);
            case 7:
                return target.$invoke$(rest[0], rest[1], rest[2], rest[3], rest[4], rest[5], array);
            }
            
            Object[] newRest = new Object[targetLength];
            System.arraycopy(rest, 0, newRest, 0, targetLength - 1);
            newRest[targetLength - 1] = array;
            return target.$invoke$(newRest);
        }
        throw fail();
    }
    
    @Override
    public boolean isWeavable(Class<?> classBeingRedefined) {
        return target.isWeavable(classBeingRedefined);
    }

    @Override
    public void weave(OptimizerContext context, OptimizerVarAllocator allocator) {
        if (Debug.DEBUG) {
            Debug.checkAllocator(allocator, this);
        }
        
        OptimizerSnippets.varargsCollect(context, allocator, methodType, targetLength, componentType, target);
    }
}
